React 文档
导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。
React 中的虚拟 DOM 是什么?有什么作用?
回答
shell
# 虚拟dom
虚拟 DOM 本质是 JS 对象,用来描述真实 DOM 的结构和属性。
# 作用
作用有两个核心:
① 避免直接操作真实 DOM(DOM 操作性能低),通过对比 VDOM 的差异(diff 算法),只更新需要变化的真实 DOM;
② 跨平台,React 可以把 VDOM 渲染到不同平台(Web / 原生 App)。虚拟 DOM(Virtual DOM)是什么?优缺点?哪些场景不优?
回答
shell
# 总结
虚拟 DOM 是 JS 对象表示的 DOM 结构,通过 Diff 计算最小更新量;优点是跨平台、简化逻辑,缺点是额外计算成本;简单场景可能不如直接操作 DOM。
# 优点
抽象 DOM 操作,让 React 可渲染到 DOM/Canvas/Native(跨平台)。
批量更新、减少 DOM 操作(DOM 操作比 JS 计算慢)。
简化状态驱动 UI 的逻辑(无需手动操作 DOM)。
# 缺点
额外的 JS 计算成本(Diff 过程),简单场景(如单一元素更新)可能比直接操作 DOM 慢。
内存占用(存储虚拟 DOM 树)。
# 不优场景
频繁更新的简单 UI(如计数器)、性能极致敏感的场景(如游戏渲染)。JSX 是什么?如何转化为真实 DOM?
回答
shell
# JSX 是什么
首先,JSX 是 React 提供的 JavaScript 语法扩展,本质是 React.createElement 函数的语法糖,它允许我们在 JS 代码中直接编写类 HTML 的结构,既保留了 HTML 的直观性,又能结合 JS 的逻辑能力(比如通过 {} 嵌入表达式)。需要注意的是,JSX 不是标准 JS,浏览器无法直接解析,必须通过 Babel 编译后才能运行,且它有一些和 HTML 不同的语法规范,比如用 className 代替 class、标签必须闭合等。
# 如何转化为真实 DOM?
其次,JSX 转化为真实 DOM 主要分为三个阶段:
编译阶段:Babel 会把 JSX 语法编译成 React.createElement 函数调用,这个函数的参数包括元素类型、属性对象、子节点等;
创建虚拟 DOM:React.createElement 执行后,会返回一个描述真实 DOM 结构的纯 JS 对象(即虚拟 DOM),它是真实 DOM 的轻量级副本,操作成本远低于真实 DOM;
渲染为真实 DOM:React 通过 ReactDOM.createRoot(React 18+)或 ReactDOM.render 方法,遍历虚拟 DOM 对象,逐个创建对应的真实 DOM 节点,设置属性和子节点,最终将根节点挂载到页面的指定容器中。React 的 diff 算法原理?对比过程?
回答
shell
react的diff算法遵循三个核心逻辑分别是:
1. 类型决定复用:如果两个节点的类型(如标签名、组件名)不同,则直接销毁旧节点并创建新节点,不再深入对比子节点。
2. 同层节点对比:只对比同一层级的节点,不跨层级比较(如果节点跨层级移动,会被视为删除旧节点 + 创建新节点)。
3. key 标识唯一性:同一层级的节点通过 key 属性标识唯一性,key 相同的节点被认为是同一节点,可复用并对比细节;key 不同则视为新节点。
具体对比流程
1. 根(单)节点对比
类型不同:直接销毁旧根节点及其整个子树,创建新根节点及其子树(最耗时,应避免频繁变更根节点类型)。
类型相同:复用根节点,继续对比其属性(如 className、style 等),更新差异属性,然后递归对比子节点。
2. 同一层级多个子节点对比
假设新旧虚拟 DOM 中,同一层级的子节点列表分别为 oldChildren 和 newChildren,对比过程分以下几种情况:
子节点数为 0 的情况
若 newChildren 为空:删除 oldChildren 中所有节点。
若 oldChildren 为空:创建 newChildren 中所有节点。
子节点数不为 0 的情况
React 通过「双指针遍历」优化对比效率,定义两个指针 oldIndex(指向 oldChildren 起始)和 newIndex(指向 newChildren 起始),同时记录 oldChildren 中节点的 key 与索引的映射(oldKeyToIndexMap),用于快速查找
- 遍历新列表,查找可复用节点:
对 newChildren[newIndex],通过其 key 在 oldKeyToIndexMap 中查找对应 oldIndex。
若找到且节点类型相同:复用该节点,更新属性,递归对比其子节点,然后移动指针(oldIndex++,newIndex++)。
若未找到(key 不存在或类型不同):
若 oldIndex 未越界,且 oldChildren[oldIndex] 与当前新节点不匹配:标记 oldChildren[oldIndex] 为待删除,oldIndex++。
若 oldIndex 已越界:直接创建新节点,newIndex++。
- 处理剩余节点:
若 oldIndex 未遍历完 oldChildren:删除剩余旧节点。
若 newIndex 未遍历完 newChildren:创建剩余新节点。
3. key 是同一层级节点唯一标识,直接影响 Diff 效率
正确使用 key(如稳定唯一的 ID):React 能快速找到可复用节点,避免不必要的创建 / 删除,尤其在列表排序、过滤时(如 key 不变,只需移动节点位置,无需重新创建)。
错误使用 key(如用索引 index 作为 key):当列表增删或排序时,key 会随位置变化,导致 React 误判节点为新节点,频繁销毁和创建,引发性能问题或状态丢失(如输入框内容重置)。
总结:综上 react的diff算法,把 O (n³) 复杂度降到 O (n),提升性能。React 的 Diff 算法特点?为什么列表要加 key?
回答
shell
# 总结
Diff 算法基于「同级比较」「key 复用」优化,比传统 Diff 效率更高;key 用于标识节点唯一性,避免错误复用。
# 特点
传统 Diff 是 O (n³) 复杂度(对比所有节点),React Diff 简化为 O (n):
只比较同级节点,不同层级节点直接销毁重建(假设跨层级移动少)。
同类型组件复用 DOM 结构,比较属性;不同类型组件直接替换。
# key 的作用
列表渲染时,key 是节点的唯一标识。没有 key 时,React 会按索引对比,导致插入 / 删除元素时误复用节点(如列表项顺序变化,内容错乱);有 key 时,React 能精准找到可复用节点,减少 DOM 操作。react 中 key 的作用
回答
shell
React 中 key 的核心作用,是给列表渲染的元素 / 组件一个稳定且唯一的身份标识,核心服务于 React 的虚拟 DOM Diff 算法。
它主要解决两个核心问题:
提升渲染性能:有了唯一 key,React 在新旧虚拟 DOM 对比时,能快速精准定位到同一个节点,只做必要的更新,避免大量不必要的 DOM 销毁与重建。
保证状态正确性:如果没有正确的 key,当列表发生增删、排序、过滤时,React 会错误复用节点,导致输入框内容、勾选状态等组件内部 state 发生错乱,出现非预期的 bug。
日常开发的最佳实践,是用后端返回的唯一 ID、UUID 这类稳定不变的值作为 key,尽量不要用数组索引 index 作为 key。拓展
shell
# 为什么不能用 index 作为 key?
当列表发生增删、排序、倒序插入等操作时,数组索引会对应到不同的节点,key 的身份标识完全失效。React 会错误地认为索引对应的是同一个节点,引发状态错乱;同时大量节点被错误销毁重建,渲染性能大幅下降,尤其是头部插入、列表倒序的场景,会出现性能断崖式下跌。
# key 重复了会怎么样?
首先 React 会在控制台抛出警告;其次重复的 key 会导致 React 无法正确区分节点,出现节点复用异常、渲染内容错乱、状态丢失等问题,极端情况下会引发列表渲染崩溃。
# 列表里的元素没有唯一 ID,怎么办?
优先方案是在数据生成时给每一项添加唯一 UUID;如果是纯静态、无增删排序的列表,可临时用 index 作为 key;也可以用内容的哈希值(比如多个字段拼接后的唯一值)作为 key,绝对不要用随机数。
# 列表里的元素没有唯一 ID,怎么办?
优先方案是在数据生成时给每一项添加唯一 UUID;如果是纯静态、无增删排序的列表,可临时用 index 作为 key;也可以用内容的哈希值(比如多个字段拼接后的唯一值)作为 key,绝对不要用随机数。